using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using MathSharp;
using MathSharp.Utils;
using System.Reflection;

namespace MathSharp.Script
{
	public class DeclareCommand: BaseCommand
	{
		public DeclareCommand(Namespace ns): base(ns)
		{
		}
		
		public static DeclareCommand SelectDeclareCommand(Namespace ns, List<string> scriptText) {
			string firstLine = scriptText[0];
			string[] flElements = firstLine.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
			if (flElements.Length < 3)
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), 0, 0x0,
				                         "Ошибка синтаксиса команды declare. Ожидается следующий формат: declare [declre type] [declare name]");
			string declareType = flElements[1];
			//string declareName = flElements[2];
			
			switch (declareType.ToLower()) {
				case "operator":
					return new DeclareOperatorCommand(ns);
				case "function":
					return new DeclareFunctionCommand(ns);
				case "tuple":
					return new DeclareTupleCommand(ns);
				case "priority":
					return new DeclarePriorityCommand(ns);
				default :
					throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), 0, 0x0,
					                         "Встретился неизвестный тип оператора declare: {0}", declareType);
			}
		}
		
		public override void Parse (IList<string> scriptText)
		{
			base.Parse(scriptText);
			string firstLine = scriptText[0];
			string[] flElements = firstLine.Split(new char[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
			if (flElements.Length < 3)
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), 0, 0x0,
				                         "Ошибка синтаксиса команды declare. Ожидается следующий формат: declare [declre type] [declare name]");
			DeclareType = flElements[1];
			DeclareName = flElements[2];
		}
		
		public string DeclareType {
			get;
			protected set;
		}
		
		public string DeclareName {
			get;
			protected set;
		}
		
		public override Value Execute ()
		{
			// декларе блоки не выполняемы и не возвращают результат
			base.Execute ();
			return null;
		}
		
		public override string Name {
			get {
				return "declare {0} {1}".format(DeclareType, DeclareName);
			}
		}
	}
	
	public abstract class DeclareExecutableCommand: DeclareCommand {
		public class Argument {
			protected Argument(ValueType type, string name) {
				this.Type = type;
				this.Name = name;
			}
			
			public ValueType Type {
				get;
				protected set;
			}
			
			public string Name {
				get;
				protected set;
			}
			
			public static Argument Read(ParsableStringReader str) {
				str.SkipWhitespaceChars();
				if (str.CurrentChar == ')')
					return null;
				if (str.EOF)
					throw new ParseException(null, 0, str.Offset, 0x0, "Неожиданное окончание строки. Ожтдалось )");
				string typeName = str.ReadToWhitespace();
				ValueType type = ValueType.Variable;
				switch (typeName.ToLower()) {
					case "int":
						type = ValueType.Int;
						break;
					case "float":
						type = ValueType.Double;
						break;
					case "bool":
						type = ValueType.Bool;
						break;
					case "string":
						type = ValueType.String;
						break;
					case "array":
						type = ValueType.Array;
						break;
					case "dictionary":
						type = ValueType.Dictionary;
						break;
					case "cube":
						type = ValueType.Cube;
						break;
					case "void":
						type = ValueType.Void;
						break;
					case "var":
						type = ValueType.Variable;
						break;
					default:
						throw new ParseException(null, 0, str.Offset, 0x0,
						                         "Встретился неизвестный тип аргумента: {0}", typeName);
				}
				str.SkipWhitespaceChars();
				if (str.CurrentChar != '$')
					throw new ParseException(null, 0, str.Offset, 0x0, "Имя аргумента должно начинться со знака $");
				str.Seek(1);
				string name = str.ReadToWhitespace(new char[] { ',', ')' });
				if (name.Contains(' '))
					throw new ParseException(null, 0, str.Offset, 0x0, "Имя аргумента не должно содиржать пробелов");
				
				return new Argument(type, name);
			}
		}
		
		public enum DeclareExecutableType {
			CSharp,
			MSharp,
			External,
			Native
		}
		
		public DeclareExecutableCommand(Namespace ns): base(ns) {
		}
		
		public override void Parse (IList<string> scriptText)
		{
			if (scriptText.Count < 1)
				throw new ArgumentException("Ошибка формата исплоняемого декларативного оператора: передано 0 строк.", "scriptText");
			base.Parse (scriptText);
			
			ParsableStringReader str = new ParsableStringReader(ns, scriptText[0]);
			// проверяем часть "declare"
			if (str.ReadToWhitespace().ToLower() != "declare")
				throw new ArgumentException("Переданый блок не является правильным declare блоком");
			// проверяем тип декларативного блока
			str.SkipWhitespaceChars();
			if (str.ReadToWhitespace().ToLower() != DeclareTypeName().ToLower())
				throw new ArgumentException("Переданый блок не является правильным declare {0} блоком",
				                            DeclareTypeName());
			// читаем имя
			str.SkipWhitespaceChars();
			Name = str.ReadWhile(NameReadPredicate);
			if (Name.IsNullOrWhiteSpace())
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Не удалось прочитать имя декларативного блока");
			
			// читаем описание аргументов
			str.SkipWhitespaceChars();
			if (str.CurrentChar != '(')
				throw new ParseException(null, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Ожидалась ( для обявления списка аргументов, а встретился {0}", str.CurrentChar);
			str.Seek(1);
			while (true) {
				Argument arg = Argument.Read(str);
				if (arg == null)
					break;
				args.Add(arg);
			}
			if (str.EOF || str.CurrentChar != ')') 
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Ожидалась ) для закрытия обявления списка аргументов, а встретился {0}", str.CurrentChar);
			str.Seek(1);
			
			// читаем тип декларации блока
			str.SkipWhitespaceChars();
			if (str.EOF)
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Ожидалась объявление типа декларативного блока");
			if (str.ReadToWhitespace().ToLower() != "as")
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Ожидалась as для объявление типа декларативного блока");
			str.SkipWhitespaceChars();
			string declType = str.ReadToWhitespace();
			switch (declType.ToLower()) {
				case "csharp":
					//ExecuteType = DeclareExecutableCommand.DeclareExecutableType.CSharp;
					throw new NotImplementedException("Описание функций на C# в данный момент не поддерживается");
				case "msharp":
					ExecuteType = DeclareExecutableCommand.DeclareExecutableType.MSharp;
					break;
				case "external":
					ExecuteType = DeclareExecutableCommand.DeclareExecutableType.External;
					break;
				case "native":
					//ExecuteType = DeclareExecutableCommand.DeclareExecutableType.Native;
					throw new NotImplementedException("Описание функций с ссылкой native в данный момент не поддерживается");
				default:
					throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
					                         "Неизвестный тип декларации исполняемого блока {0}", declType);
			}
			if (ExecuteType == DeclareExecutableCommand.DeclareExecutableType.External ||
			    ExecuteType == DeclareExecutableCommand.DeclareExecutableType.Native) { // читаем доппараметры
				str.SkipWhitespaceChars();
				string tmp = str.ReadToWhitespace();
				if (tmp.IsNullOrWhiteSpace())
					throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
					                         "Ожидалась имя внешней функции");
				if (tmp.ToLower().StartsWith("API")) {
					APIVersion = tmp.Substring(3);
					str.SkipWhitespaceChars();
					tmp = str.ReadToWhitespace();
					if (tmp.IsNullOrWhiteSpace())
						throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
						                         "Ожидалась имя внешней функции");
				}
				
				ExternalFunction = tmp;
			}
			
			str.SkipWhitespaceChars();
			if (!str.EOF)
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Излишнее неизвестное описание функции: {0}", str.ReadToEnd());
		}
		
		protected abstract bool NameReadPredicate(char c, int offset);
		protected abstract string DeclareTypeName();
		
		public string Name {
			get;
			protected set;
		}
		
		protected List<Argument> args = new List<Argument>();
		
		public List<Argument> Arguments {
			get {
				return args;
			}	
		}
			
		public DeclareExecutableType ExecuteType {
			get;
			protected set;
		}
		
		public string APIVersion {
			get;
			protected set;
		}
		
		public string ExternalFunction {
			get; 
			protected set;
		}

		public override void PrepareExecute ()
		{
			base.PrepareExecute ();
		}
		
		/// <summary>
		/// Запускает выполнаемый блок на выполнение
		/// </summary>
		public Value ExecuteDeclaration(IEnumerable<Value> args) {
			switch (ExecuteType) {
				case DeclareExecutableType.CSharp:
					throw new NotImplementedException();
				case DeclareExecutableType.MSharp:
					throw new NotImplementedException();
				case DeclareExecutableType.External:
					return ExecuteDeclarationExternal(args);
				case DeclareExecutableType.Native:
					throw new NotImplementedException();
				default:
					throw new ExecuteException(Script.Filename, 0, 0, 0x0,
					                           "Невозможно выполнит функцию/оператор {0} т.к. указан неизвестный тип декларации",
					                           Name);
			}
		}
		
		Type externalType;
		MethodInfo externalMethod;
		bool externalPrepared = false;
		object externalLockObject = new object();
		
		void externalPrepare() {
			if (externalPrepared)
				return;
			
			lock (externalLockObject) {
				if (externalPrepared)
					return;
				
				int pos = ExternalFunction.IndexOf(':');
				if (pos < 0)
					throw new ExecuteException(script.FileName, 0, 0, 0x0,
					                           "В функции {0} не верно указанна внешняя функция {1}. Имя класса должно быть отделенно от метода символом :",
					                           Name, ExternalFunction);
				string className = ExternalFunction.Substring(0, pos);
				string methodName = ExternalFunction.Substring(pos+1);
				externalType = Type.GetType(className, false, false);
				if (externalType == null)
					throw new ExecuteException(script.FileName, 0, 0, 0x0,
					                           "Не найден класс {0} для external функции {1}",
					                           className, ExternalFunction);
				List<Type> argTypes = new List<Type>();
				argTypes.Add(typeof(Namespace));
				Type valueType = typeof(Value);
				for (int i = 0; i < Arguments.Count; i++)
					argTypes.Add(valueType);
				externalMethod = externalType.GetMethod(methodName, argTypes);
				if (externalMethod == null)
					throw new ExecuteException(script.FileName, 0, 0, 0x0,
					                           "Не найден метод {0} класса {1} для external функции {2}",
					                           methodName, className, ExternalFunction);
				if (externalMethod.ReturnType != valueType)
					throw new ExecuteException(script.FileName, 0, 0, 0x0,
					                           "Возвращаемый тип метода {0} класса {1} для external функции {2} не возвращает значение типа Value",
					                           methodName, className, ExternalFunction);
			}
			externalPrepared = true;
		}
		
		public Value ExecuteDeclarationExternal(IEnumerable<Value> args) {
			if (args == null)
				throw new ArgumentNullException("args");
			if (Arguments.Count != args.Count)
				throw new ExecuteException(script.FileName, 0, 0, 0x0,
				                           "Некоректное количество аргументов при вызове функции {0}. Требуется {1}. Было переданно {2}.",
				                           Name, Arguments.Count, args.Count());
			externalPrepare();
			List<object> eArgs = new List<object>();
			eArgs.Add(ns);
			eArgs.AddRange(args.Cast<object>());
			Value res = externalMethod.Invoke(null, eArgs) as Value;
			// TODO возможно нужны доп. проверки на null?
			return res;
		}
	}
	
	public class DeclareOperatorCommand: DeclareExecutableCommand {
		public DeclareOperatorCommand(Namespace ns): base(ns) {
			nameRegexString = @"[+\-*/%\\!^~&|\?:<>]+";
			declareTypeRegexString = "operator";
		}
		
		public override void Parse (IList<string> scriptText)
		{
			base.Parse(scriptText);
		}
		
		public override void PrepareExecute ()
		{
			base.PrepareExecute();
		}
		
		public override Value ExecuteDeclaration(IEnumerable<Value> args) {
			throw new NotImplementedException();
		}
	}
	
	public class DeclareFunctionCommand: DeclareExecutableCommand {
		public DeclareFunctionCommand(Namespace ns): base(ns) {
			nameRegexString = @"[a-zA-Z_][a-zA-Z0-9_]?";
			declareTypeRegexString = "function";
		}
		
		public override void Parse (IList<string> scriptText)
		{
			base.Parse(scriptText);
		}
		
		public override void PrepareExecute ()
		{
			base.PrepareExecute();
		}

		public override Value ExecuteDeclaration(IEnumerable<Value> args) {
			throw new NotImplementedException();
		}
	}
	
	public class DeclareTupleCommand: DeclareCommand {
		public DeclareTupleCommand(Namespace ns): base(ns) {
		}
		
		public override void Parse (IList<string> scriptText)
		{
			throw new NotImplementedException ();
		}
		
		public override void PrepareExecute ()
		{
			base.PrepareExecute ();
		}
	}
	
	public class DeclarePriorityCommand: DeclareCommand {
		public DeclarePriorityCommand(Namespace ns): base(ns) {
		}
		
		public override void Parse (IList<string> scriptText)
		{
			if (scriptText.Count == 0)
				throw new ArgumentException("Исходный код команды содержит 0 строк");
			if (scriptText.Count > 1)
				throw new ParseException(scriptText, scriptText.GetOriginalLineSmarts(0), 0, 0x0, "Команда declare priority не может быть более 1 строки");
			ParsableStringReader str = new ParsableStringReader(script.Namespace, scriptText[0]);
			// проверяем два первых слова declare priority
			str.SkipWhitespaceChars();
			string d1 = str.ReadToWhitespace().ToLower();
			if (d1 != "declare")
				throw new ArgumentException("Предоставленная команда не является командой declare");
			str.SkipWhitespaceChars();
			string d2 = str.ReadToWhitespace().ToLower();
			if (d2 != "priority")
				throw new ArgumentException("Предоставленная команда не является командой declare priority");
			
			str.SkipWhitespaceChars();
			string op = str.ReadToWhitespace();
			str.SkipWhitespaceChars();
			object pd = str.ReadNumericDeclaration();
			if (!pd.GetType().IsAnyInteger())
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Описание приоритета должно быть целочисленным числом");
			str.SkipWhitespaceChars();
			if (!str.EOF)
				throw new ParseException(script.FileName, scriptText.GetOriginalLineSmarts(0), str.Offset, 0x0,
				                         "Ожидался конец строки");
			try {
				OperationPriorityTable.Instance.Add(op, (int)pd);
			} catch (ParseException pe) {
				throw pe.Rethrow(script.FileName, scriptText.GetOriginalLineSmarts(0), 0, 0x0);
			}
		}
		
		public override void PrepareExecute ()
		{
			base.PrepareExecute ();
		}
	}
}

